package rootkit

import (
	"fmt"
	"os"
	"syscall"
	"io/ioutil"
	"path/filepath"
	"strings"
)

// syscheck主要体现在以下几个方面：
// 1. 检查范围：
//		- 全盘检查，基于"/"开始全量扫描；速度较慢；
// 		- 指定目录扫描，基于配置决定；todo：利用web后台执行管理配置；
// 2. rootkit匹配条件
//		- 文件对象名和rootkit库匹配，匹配中，则提供告警；todo：rootkit库需要web后台配置管理维护；
//		- nlink、readdir中的条目个数是否一致，不一致也可能存在异常，需告警；
//		- readdir中文件存在，但是stat文件不存在，则可能存在异常；
//		- read的文件大小和stat中的size不一致，则可能存在异常；
// 		- root用户的文件，但是other用户可写；

type SysCheck struct {
	BaseDir 	string
	ScanAll 	bool
	ReadAll 	bool

	IgnoreSet 	map[string]struct{}
	SkipNFS 	bool
	DevId 			int32					// device id

	RkSysFileSet 	map[string]*KitSysFile

	Stat 			StatInfo				// 统计信息
}

type KitSysFile struct {
	FileName 		string
	SysName 		string
}

var dirsToScan = []string{
	"/bin",
	"/sbin",
	"/usr/bin",
	"/usr/sbin",
	"/usr/lib",
	"/usr/include",
	"/usr/local",
	"/dev",
	"/lib",
	"/etc",
	"/root",
	"/var/log",
	"/var/mail",
	"/var/lib",
	"/var/www",
	"/var/tmp",
	"/tmp",
	"/boot",
	"sys",
}
var dirsToDoRead = []string{
	"/bin",
	"/sbin",
	"/usr/bin",
	"/usr/sbin",
	"/dev",
	"/etc",
	"/boot",
}

// SysCheck.RcSysCheck 提供检查入口
func (s *SysCheck) RcSysCheck() error {
	s.Stat.Total = 0
	s.Stat.Errors = 0
	s.DevId = 0

	if s.ScanAll {
		// scan the whole file system -- may be slow
		// 扫描整个文件系统，可能会比较慢
		s.readSysDir("/", s.ReadAll)
	} else {
		// scan only specific directories
		// 扫描指定目录
		for _, item := range dirsToScan {
			s.readSysDir(fmt.Sprintf("%s%s", s.BaseDir, item), s.ReadAll)
		}
	}

	// 结果判断
	if s.Stat.Errors == 0 {
		// TODO: 实现处理
		fmt.Sprintf("No problem found on the system. Analyzed %d files.", s.Stat.Total)
	}

	return nil
}

// SysCheck.readSysDir 分析目录
func (s *SysCheck) readSysDir(dirName string, doRead bool) error {
	if dirName == "" {
		return fmt.Errorf("invalid directory given")
	}

	didChanged := false
	var entryCnt uint16

	// Ignore user-supplied list
	// 支持用户提供的忽略列表
	if len(s.IgnoreSet) > 0 {
		if _, exist := s.IgnoreSet[dirName]; exist {
			return nil
		}
	}

	// 根据配置是否检查NFS
	if s.SkipNFS {
		isNFS, err := IsNFS(dirName)
		if err != nil || isNFS {
			if err != nil {
				// TODO： 打印日志
			}
			return nil						// 判断NFS出错，或者为NFS均截止判断
		}
	}

	// Getting the number of nodes. The total number on opendir must be the same
	// 读取dir的信息
	dirInfo, err := os.Lstat(dirName)
	if err != nil {
		return err
	}

	// 获取当前的设备id
	devId := dirInfo.Sys().(syscall.Stat_t).Dev
	if s.DevId != devId {
		if s.DevId != 0 {
			didChanged = true
		}
		s.DevId = devId				// 更新记录的dev id
	}
	if dirInfo.IsDir() == false {
		return nil					// 不是dir，则停止
	}

	// Check if the `doRead` is valid for this directory
	for _, item := range dirsToDoRead {
		if item == dirName {
			doRead = true
			break
		}
	}

	// Open the directory，读取全部目录
	fileInfos, err := ioutil.ReadDir(dirName)
	if err != nil {
		return err
	}

	// 遍历目录里面的所有实例，Read every entry in the directory
	for _, item := range fileInfos {
		// 忽略.和..目录
		if item.Name() == "." || item.Name() == ".." {
			entryCnt++					// TODO：该处需要验证正确与否，因为实际上不一定会有.和..的item
			continue
		}

		// create new file + path string
		fileName := filepath.Join(dirName, item.Name())
		// 检查文件是否是目录
		if fileInfo, err := os.Lstat(fileName); err == nil {
			if fileInfo.IsDir() {
				entryCnt++
			}
		}

		// Check every file against the rootkit database
		// 检查每一个文件是否和rootkit的db中的数据是否匹配，匹配的直接告警alert
		if rk, exist := s.RkSysFileSet[item.Name()]; exist {
			// TODO: 告警信息
			fmt.Sprintf("Rootkit '%s' detected by the presence of file '%s/%s'.", rk.SysName, dirName, rk.FileName)
			s.Stat.Errors++
		}

		// ignore the /proc and /sys file systems
		if fileName == "/proc" || fileName == "/sys" {
			continue
		}

		// 进行文件分析
		s.readSysFile(fileName, doRead)
	}

	// skip further test because the FS cant deliver the stats (btrfs link count always is 1)
	skip, err := SkipNFS(dirName)
	if err != nil || skip == true {
		return nil						// 出错、或者skip，则停止
	}

	// 检查dir中条目数量 和 实际stat连接条数相同
	nlink := dirInfo.Sys().(syscall.Stat_t).Nlink
	if (entryCnt != nlink) && ((didChanged == false) || (entryCnt + 1) != nlink) {
		// 不一样，则重新获取
		if dirInfo2, err := os.Lstat(dirName); err == nil {
			if dirInfo2.Sys().(syscall.Stat_t).Nlink != entryCnt {
				// TODO: 告警信息
				 fmt.Sprintf("Files hidden inside directory '%s'. Link count does not match number of files (%d,%d).", dirName, entryCnt, nlink)
				 s.Stat.Errors++
			}
		}
	}

	return nil
}

func (s *SysCheck) readSysFile(fileName string, doRead bool) error {
	s.Stat.Total++

	fileInfo, err := os.Lstat(fileName)
	if err != nil {
		// TODO: 告警信息
		fmt.Sprintf("anomaly detected in file '%s'. Hidden from stats, but showing up on readdir. Possible kernel level rootkit", fileName)
		s.Stat.Errors++
		return nil
	}

	// 如果是目录文件，则继续处理
	if fileInfo.IsDir() {
		/* Make Darwin happy. For some reason,
         * when I read /dev/fd, it goes forever on
         * /dev/fd5, /dev/fd6, etc.. weird
         */
		if strings.HasPrefix(fileName, "/dev/fd") {
			return nil
		}
		if fileInfo.Size() == 0 {
			return nil
		}

		return s.readSysDir(fileName, doRead)
	}

	// 非目录文件，继续处理
	// 检查stat中文件的size和read该文件的size是否一致
	if fileInfo.Mode().IsRegular() && doRead {
		if fileName == "/dev/bus/usb/.usbfs/devices" {
			// 不处理
		} else {
			// read，读出文件的实际长度
			var total int64
			if fd, err := os.Open(fileName); err == nil {
				buf := make([]byte, 0, 1024)
				for {
					if n, err := fd.Read(buf); err != nil || n <= 0 {
						break
					} else {
						total = total + int64(n)
					}
				}
				fd.Close()
			}

			if total != fileInfo.Size() {
				if fileInfo2, err := os.Stat(fileName); err == nil {
					// 两次stat的长度一致，但是和真是read的长度不一致
					if total != fileInfo2.Size() && fileInfo.Size() == fileInfo2.Size() {
						// TODO: 告警处理
						fmt.Sprintf("Anomaly detected in file '%s'. File size doesn't match what we found. Possible kernel level rootkit.", fileName)
						s.Stat.Errors++
					}
				}
			}
		}
	}

	// 如果有OTHER的write和exec的权限，也要告警alert
	if (fileInfo.Mode().Perm() & WOther) == WOther && fileInfo.Mode().IsRegular() {
		if fileInfo.Mode().Perm() & XUser == XUser {
			// TODO: 告警处理
			fmt.Sprintf("File '%s' exec by user, but other can write.", fileName)
			s.Stat.Errors++
		}

		if fileInfo.Sys().(syscall.Stat_t).Uid == 0 {
			// TODO: 告警处理
			fmt.Sprintf("File '%s' is owned by root and has write permissions to anyone.", fileName)
			s.Stat.Errors++
		}
	}

	return nil
}
